/** * List block tracker for UUID-aware list editing and serialization. * * Dependencies: wp.blocks, SFE.ElementPrep * Exposes: SFE.ListBlockTracker */ (function() { 'use strict'; window.MWP = window.MWP || {}; window.MWP.SFE = window.MWP.SFE || {}; const SFE = window.MWP.SFE; SFE.ManagerData = SFE.ManagerData || {}; const { createBlock, serialize: serializeBlocks } = window.wp?.blocks || {}; const UUID_ATTR_KEYS = ['mwpSfeUuid', 'mwpSfeUuidShadow']; const LIST_ITEM_TEXT_ATTR = 'data-mwp-sfe-list-item-text'; const LIST_ITEM_TEXT_CLASS = 'mwp-sfe-list-item-text'; const PLACEHOLDER_ATTR = 'data-rich-text-placeholder'; const LIST_ID_ATTR = 'data-list-id'; const LIST_ITEM_ID_ATTR = 'data-item-id'; const LIST_RUNTIME_UUID_ATTR = 'data-mwp-sfe-list-runtime-uuid'; const LIST_ITEM_RUNTIME_UUID_ATTR = 'data-mwp-sfe-list-item-runtime-uuid'; /** * Return whether one node is a nested list element. * * @param {Node|null} node Candidate DOM node. * @returns {boolean} True when the node is a nested list. */ function isNestedListNode(node) { return !!( node && node.nodeType === Node.ELEMENT_NODE && (node.tagName === 'UL' || node.tagName === 'OL') ); } /** * Return whether one node is ignorable formatting whitespace between list * structures. * * Pretty-printed block markup often leaves direct `\n` text nodes between a * list item's own text and its nested child list. Those nodes should not be * moved into the direct text surface because they become visible editing * artifacts once wrapped. * * @param {Node|null} node Candidate DOM node. * @returns {boolean} True when the node is ignorable whitespace. */ function isIgnorableListWhitespaceNode(node) { return !!( node && node.nodeType === Node.TEXT_NODE && String(node.textContent || '') .replace(/\uFEFF/g, '') .replace(/\u00A0/g, ' ') .trim() .length === 0 ); } /** * Return the direct inline text surface for one list item. * * @param {HTMLLIElement|null} liElement Candidate list item. * @returns {HTMLElement|null} Existing direct text surface, if any. */ function getDirectItemTextSurface(liElement) { if (!liElement || liElement.nodeType !== Node.ELEMENT_NODE || liElement.tagName !== 'LI') { return null; } return Array.from(liElement.children || []).find(child => ( child && child.nodeType === Node.ELEMENT_NODE && child.getAttribute(LIST_ITEM_TEXT_ATTR) === '1' )) || null; } /** * Return the DOM attribute name that stores one session-scoped runtime UUID. * * Runtime UUIDs are distinct from the existing serialization/attr UUIDs. They * exist only while the editor session is open so external callers can point at * the current live list cursor/target without having to manage shifting paths. * * @param {string} type Supported runtime identity type. * @returns {string} Attribute name, or an empty string for invalid types. */ function getRuntimeUuidAttributeName(type) { if (type === 'list') { return LIST_RUNTIME_UUID_ATTR; } if (type === 'item') { return LIST_ITEM_RUNTIME_UUID_ATTR; } return ''; } /** * Normalize one runtime UUID candidate. * * @param {*} value Candidate runtime UUID. * @returns {string} Trimmed runtime UUID, or an empty string. */ function normalizeRuntimeUuid(value) { return String(value || '').trim(); } /** * Return whether one element matches the requested runtime-identity type. * * @param {Element|null} element Candidate DOM element. * @param {string} type Supported runtime identity type. * @returns {boolean} True when the element matches the type. */ function isRuntimeIdentityElement(element, type) { if (!element || element.nodeType !== Node.ELEMENT_NODE) { return false; } if (type === 'list') { return element.tagName === 'UL' || element.tagName === 'OL'; } if (type === 'item') { return element.tagName === 'LI'; } return false; } /** * Return one tracked element by runtime UUID from the live DOM. * * This intentionally queries the current DOM first instead of trusting only a * cached map so history restores and other structural replacements can keep * runtime UUID resolution stable as long as the attributes remain present. * * @param {Object|null} tracker Active list tracker. * @param {string} runtimeUuid Session-scoped runtime UUID. * @param {string} type Supported runtime identity type. * @returns {Element|null} Matching live DOM element. */ function findElementByRuntimeUuid(tracker, runtimeUuid, type) { const normalizedRuntimeUuid = normalizeRuntimeUuid(runtimeUuid); const attrName = getRuntimeUuidAttributeName(type); if ( !tracker?.listElement || !normalizedRuntimeUuid || !attrName || !isRuntimeIdentityElement(tracker.listElement, 'list') ) { return null; } if ( type === 'list' && tracker.listElement.getAttribute(attrName) === normalizedRuntimeUuid ) { return tracker.listElement; } try { const selector = `[${attrName}="${CSS.escape(normalizedRuntimeUuid)}"]`; return tracker.listElement.querySelector(selector); } catch (error) { const escapedUuid = normalizedRuntimeUuid.replace(/"/g, '\\"'); return tracker.listElement.querySelector(`[${attrName}="${escapedUuid}"]`); } } /** * Return every direct text surface currently attached to one list item. * * Merge/delete flows can temporarily move multiple direct text surfaces into * the same list item. The shared list normalizer must collapse them back to * one canonical surface before placeholder syncing or serialization. * * @param {HTMLLIElement|null} liElement Candidate list item. * @returns {HTMLElement[]} Direct text surfaces in DOM order. */ function getAllDirectItemTextSurfaces(liElement) { if (!liElement || liElement.nodeType !== Node.ELEMENT_NODE || liElement.tagName !== 'LI') { return []; } return Array.from(liElement.children || []).filter(child => ( child && child.nodeType === Node.ELEMENT_NODE && child.getAttribute(LIST_ITEM_TEXT_ATTR) === '1' )); } /** * Return whether one node is only placeholder/caret scaffolding. * * Duplicate direct text surfaces may carry empty placeholder anchors or * caret `
` nodes. Those artifacts should be dropped when collapsing * duplicate surfaces instead of being concatenated into visible stacks. * * @param {Node|null} node Candidate DOM node. * @returns {boolean} True when the node is redundant placeholder UI. */ function isRedundantSurfaceArtifact(node) { if (!node) { return true; } if (isIgnorableListWhitespaceNode(node)) { return true; } if (node.nodeType === Node.TEXT_NODE) { return String(node.textContent || '') .replace(/\uFEFF/g, '') .replace(/\u00A0/g, ' ') .trim() .length === 0; } if (node.nodeType !== Node.ELEMENT_NODE) { return false; } if (node.tagName === 'BR') { return true; } return !!node.hasAttribute?.(PLACEHOLDER_ATTR); } /** * Ensure one list item owns one direct inline text surface. * * The root list remains the single live editor host, but this wrapper gives * schema/ABE flows one stable DOM surface per list item's own text content. * * @param {HTMLLIElement|null} liElement Candidate list item. * @returns {HTMLElement|null} Ensured direct text surface. */ function ensureDirectItemTextSurface(liElement) { if (!liElement || liElement.nodeType !== Node.ELEMENT_NODE || liElement.tagName !== 'LI') { return null; } let surface = getDirectItemTextSurface(liElement); if (!surface) { surface = document.createElement('span'); surface.setAttribute(LIST_ITEM_TEXT_ATTR, '1'); surface.classList.add(LIST_ITEM_TEXT_CLASS); liElement.insertBefore( surface, Array.from(liElement.childNodes || []).find(isNestedListNode) || null ); } getAllDirectItemTextSurfaces(liElement) .filter(candidate => candidate && candidate !== surface) .forEach(duplicateSurface => { Array.from(duplicateSurface.childNodes || []).forEach(node => { if (isRedundantSurfaceArtifact(node)) { node.remove(); return; } surface.appendChild(node); }); duplicateSurface.remove(); }); const childNodes = Array.from(liElement.childNodes || []); childNodes.forEach(node => { if ( node && node !== surface && !isNestedListNode(node) && isIgnorableListWhitespaceNode(node) ) { node.remove(); } }); const movableNodes = childNodes.filter(node => { if (!node || node === surface || isNestedListNode(node)) { return false; } if (isIgnorableListWhitespaceNode(node)) { return false; } return !( node.nodeType === Node.ELEMENT_NODE && node.getAttribute?.(LIST_ITEM_TEXT_ATTR) === '1' ); }); movableNodes.forEach(node => surface.appendChild(node)); return surface; } /** * Clone list attrs while optionally stripping plugin UUID ownership. * * The outermost core/list is the only persisted UUID owner for an entire * list tree. Nested core/list blocks are structural children of that root. * If we preserve a nested list UUID here, one accidental assignment can be * re-serialized forever and split a single logical list into multiple * history/edit targets. Keep this guard unless the ownership model changes * everywhere else in PHP and JS at the same time. * * @param {Object} attrs Parsed Gutenberg attrs. * @param {boolean} allowUuidOwnership True for the root list only. * @returns {Object} Safe cloned attrs. */ function cloneListAttrs(attrs, allowUuidOwnership = true) { const clonedAttrs = JSON.parse(JSON.stringify(attrs || {})); if (allowUuidOwnership) { return clonedAttrs; } UUID_ATTR_KEYS.forEach(key => delete clonedAttrs[key]); return clonedAttrs; } /** * Return the direct list-item children for one list element. * * @param {HTMLElement|null} listElement Candidate list element. * @returns {HTMLLIElement[]} Direct child list items. */ function getDirectListItems(listElement) { if (!listElement || listElement.nodeType !== Node.ELEMENT_NODE) { return []; } return Array.from(listElement.children || []).filter(child => child.tagName === 'LI'); } /** * Normalize one path-like value into zero-based list indexes. * * Supported inputs: * - `0_1_2` * - `1.2.3` * - arrays of integers * * @param {string|Array|null} pathValue Candidate path value. * @returns {number[]|null} Parsed zero-based indexes. */ function normalizePathIndexes(pathValue) { if (Array.isArray(pathValue)) { const indexes = pathValue.map(value => Number.parseInt(value, 10)); return indexes.every(Number.isInteger) && indexes.every(index => index >= 0) ? indexes : null; } const raw = typeof pathValue === 'string' ? pathValue.trim() : ''; if (!raw) { return []; } const separator = raw.includes('.') ? '.' : '_'; const parts = raw.split(separator).filter(Boolean); if (!parts.length) { return []; } const indexes = parts.map(part => Number.parseInt(part, 10)); if (!indexes.every(Number.isInteger)) { return null; } if (separator === '.') { return indexes.every(index => index > 0) ? indexes.map(index => index - 1) : null; } return indexes.every(index => index >= 0) ? indexes : null; } /** * Convert one zero-based path index list into public path metadata. * * @param {number[]} indexes Zero-based indexes. * @returns {{path: string, pathLabel: string, depth: number}} Path metadata. */ function buildPathMeta(indexes) { const safeIndexes = Array.isArray(indexes) ? indexes.filter(Number.isInteger) : []; return { path: safeIndexes.join('_'), pathLabel: safeIndexes.map(index => index + 1).join('.'), depth: Math.max(0, safeIndexes.length - 1), }; } /** * Return the direct child list element for one list item. * * @param {HTMLLIElement|null} listItem Candidate list item. * @returns {HTMLElement|null} Direct nested list, if present. */ function getDirectChildList(listItem) { if (!listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI') { return null; } return Array.from(listItem.children || []).find(child => ( child.tagName === 'UL' || child.tagName === 'OL' )) || null; } /** * Return the preferred nested list tag for one list item. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Parent list item. * @returns {string} `UL` or `OL`. */ function getPreferredChildListTagName(tracker, listItem) { const directChildList = getDirectChildList(listItem); if (directChildList) { return directChildList.tagName; } const parentList = listItem?.parentElement; if (parentList && (parentList.tagName === 'UL' || parentList.tagName === 'OL')) { return parentList.tagName; } return tracker?.listElement?.tagName === 'OL' ? 'OL' : 'UL'; } /** * Return one direct child list for a list item, creating it only when needed. * * Outdent can promote one item and then re-home its trailing siblings beneath * that promoted item. When the promoted item already owns a child list, those * siblings must be appended into the existing list so the DOM mirrors native * editor behavior instead of creating duplicate sibling list wrappers. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Parent list item. * @param {string} preferredTagName Fallback list tag name. * @returns {HTMLElement|null} Direct child list element. */ function ensureDirectChildList(tracker, listItem, preferredTagName = '') { if (!listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI') { return null; } let childList = getDirectChildList(listItem); if (childList) { return childList; } childList = document.createElement( preferredTagName || getPreferredChildListTagName(tracker, listItem) ); childList.classList.add('wp-block-list'); listItem.appendChild(childList); return childList; } /** * Remove empty nested list wrappers up the ancestry chain. * * The root list element is never removed, even when it becomes empty. * * @param {Object|null} tracker Active list tracker. * @param {HTMLElement|null} startList First candidate nested list. * @returns {void} */ function cleanupEmptyAncestorLists(tracker, startList) { let currentList = startList; while ( currentList && currentList !== tracker?.listElement && currentList.nodeType === Node.ELEMENT_NODE && (currentList.tagName === 'UL' || currentList.tagName === 'OL') && !getDirectListItems(currentList).length ) { const parentItem = currentList.parentElement?.tagName === 'LI' ? currentList.parentElement : null; currentList.remove(); currentList = parentItem ? parentItem.parentElement : null; } } /** * Build one new list item element from a structural operation payload. * * @param {Object|null} operation Candidate operation payload. * @returns {HTMLLIElement} New list item element. */ function buildListItemFromOperation(operation) { const li = document.createElement('li'); const directSurface = ensureDirectItemTextSurface(li); const runtimeUuid = normalizeRuntimeUuid( operation?.itemUuid ?? operation?.newItemUuid ); const html = typeof operation?.contentHtml === 'string' ? operation.contentHtml : (typeof operation?.html === 'string' ? operation.html : ''); const text = typeof operation?.contentText === 'string' ? operation.contentText : (typeof operation?.text === 'string' ? operation.text : ''); if (html) { directSurface.innerHTML = html; } else if (text) { directSurface.textContent = text; } if (runtimeUuid) { li.setAttribute(LIST_ITEM_RUNTIME_UUID_ATTR, runtimeUuid); } return li; } /** * Copy one donor item's structural/style attributes onto the destination list * item while preserving the destination runtime UUID. * * Native Enter list splitting happens inside the browser's contenteditable * engine, so the new sibling inherits the source `li` element's attributes * such as class and style automatically. API-driven insert/move operations * should mirror that behavior by cloning the donor item's `li` attributes, * except for the session-scoped runtime UUID which must stay unique. * * @param {HTMLLIElement|null} listItem Destination list item. * @param {HTMLLIElement|null} donorItem Style/structure donor item. * @returns {void} */ function copyDonorItemAttributes(listItem, donorItem) { if ( !listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI' || !donorItem || donorItem.nodeType !== Node.ELEMENT_NODE || donorItem.tagName !== 'LI' ) { return; } const destinationRuntimeUuid = normalizeRuntimeUuid( listItem.getAttribute(LIST_ITEM_RUNTIME_UUID_ATTR) ); Array.from(listItem.attributes || []).forEach(attr => { if (attr?.name === LIST_ITEM_RUNTIME_UUID_ATTR) { return; } listItem.removeAttribute(attr.name); }); Array.from(donorItem.attributes || []).forEach(attr => { if (attr?.name === LIST_ITEM_RUNTIME_UUID_ATTR) { return; } listItem.setAttribute(attr.name, attr.value); }); if (destinationRuntimeUuid) { listItem.setAttribute(LIST_ITEM_RUNTIME_UUID_ATTR, destinationRuntimeUuid); } } /** * Reassign one list item's structural ID from its destination styling * context. * * When an explicit target item is known, its structural `data-item-id` * becomes the style donor for insert-before, insert-after, move-before, and * move-after commands. This keeps the inheritance rule simple and matches the * public API's explicit `targetItemUuid` model. * * For internal list-path insert/move cases that do not resolve through one * target item, fall back to the local destination neighbors. If there is no * neighboring item at all, clear the structural ID so the next tracker rebuild * seeds a fresh item identity instead of accidentally preserving source attrs. * * @param {HTMLLIElement|null} listItem Destination list item. * @param {HTMLLIElement|null} targetItem Explicit style donor item. * @returns {string} Applied structural ID or an empty string. */ function inheritDestinationItemId(listItem, targetItem = null) { if (!listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI') { return ''; } const explicitTargetItem = targetItem && targetItem.nodeType === Node.ELEMENT_NODE && targetItem.tagName === 'LI' ? targetItem : null; const previousItem = listItem.previousElementSibling?.tagName === 'LI' ? listItem.previousElementSibling : null; const nextItem = listItem.nextElementSibling?.tagName === 'LI' ? listItem.nextElementSibling : null; const inheritedId = normalizeRuntimeUuid( explicitTargetItem?.getAttribute(LIST_ITEM_ID_ATTR) || previousItem?.getAttribute(LIST_ITEM_ID_ATTR) || nextItem?.getAttribute(LIST_ITEM_ID_ATTR) ); if (inheritedId) { listItem.setAttribute(LIST_ITEM_ID_ATTR, inheritedId); return inheritedId; } listItem.removeAttribute(LIST_ITEM_ID_ATTR); return ''; } /** * Apply one remove-list-item operation. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Target list item. * @returns {boolean} True when the mutation applied. */ function applyRemoveListItemOperation(tracker, listItem) { if (!tracker?.listElement || !listItem) { return false; } const oldParentList = listItem.parentElement; listItem.remove(); cleanupEmptyAncestorLists(tracker, oldParentList); return true; } /** * Apply one indent-list-item operation. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Target list item. * @returns {boolean} True when the mutation applied. */ function applyIndentListItemOperation(tracker, listItem) { if (!tracker?.listElement || !listItem) { return false; } const previousItem = listItem.previousElementSibling?.tagName === 'LI' ? listItem.previousElementSibling : null; if (!previousItem) { return false; } const nestedList = ensureDirectChildList(tracker, previousItem); nestedList.appendChild(listItem); return true; } /** * Apply one outdent-list-item operation. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Target list item. * @returns {boolean} True when the mutation applied. */ function applyOutdentListItemOperation(tracker, listItem) { if (!tracker?.listElement || !listItem) { return false; } const parentList = listItem.parentElement; const parentItem = parentList?.parentElement?.tagName === 'LI' ? parentList.parentElement : null; if (!parentList || !parentItem) { return false; } const ancestorList = parentItem.parentElement; const followingSiblings = []; let next = listItem.nextElementSibling; while (next) { followingSiblings.push(next); next = next.nextElementSibling; } ancestorList.insertBefore(listItem, parentItem.nextElementSibling); if (followingSiblings.length) { const nestedList = ensureDirectChildList(tracker, listItem, parentList.tagName); followingSiblings.forEach(sibling => nestedList.appendChild(sibling)); } cleanupEmptyAncestorLists(tracker, parentList); return true; } /** * Apply one toggle-list-type operation. * * @param {Object|null} tracker Active list tracker. * @param {Object} rawOperation Structural operation payload. * @param {Object} options Apply-operation options. * @returns {boolean} True when the mutation applied. */ function applyToggleListTypeOperation(tracker, rawOperation, options = {}) { if (!tracker?.listElement) { return false; } const listPath = rawOperation.listPath ?? rawOperation.list_path ?? ''; const targetList = getListByPath(tracker, listPath); const editorHost = options?.editorHost && typeof options.editorHost.changeListType === 'function' ? options.editorHost : null; const requestedType = targetList ? ( normalizeListTypeTagName( rawOperation.value ?? rawOperation.listType ?? rawOperation.list_type ?? rawOperation.ordered ) || (targetList.tagName === 'OL' ? 'UL' : 'OL') ) : ''; if ( !targetList || !requestedType || targetList.tagName === requestedType || !editorHost ) { return false; } const nextList = editorHost.changeListType(targetList, requestedType.toLowerCase(), { saveHistory: options.saveHistory !== false, restoreCursor: options.restoreCursor !== false, }); return !!nextList; } /** * Apply one update-list-item-text operation. * * @param {HTMLLIElement} listItem Target list item. * @param {Object} rawOperation Structural operation payload. * @returns {boolean} True when the mutation applied. */ function applyUpdateListItemTextOperation(listItem, rawOperation) { if (!listItem) { return false; } const directSurface = ensureDirectItemTextSurface(listItem); const html = typeof rawOperation?.contentHtml === 'string' ? rawOperation.contentHtml : (typeof rawOperation?.html === 'string' ? rawOperation.html : ''); const text = typeof rawOperation?.contentText === 'string' ? rawOperation.contentText : (typeof rawOperation?.text === 'string' ? rawOperation.text : ''); if (!directSurface) { return false; } directSurface.innerHTML = ''; if (html) { directSurface.innerHTML = html; } else if (text) { directSurface.textContent = text; } return true; } /** * Insert one new list item relative to explicit before/after/list anchors. * * @param {Object|null} tracker Active list tracker. * @param {Object} rawOperation Structural operation payload. * @param {Object} trackerApi List tracker API surface. * @returns {boolean} True when the mutation applied. */ function applyInsertListItemOperation(tracker, rawOperation, trackerApi) { if (!tracker?.listElement || !trackerApi) { return false; } const beforePath = rawOperation.beforePath ?? rawOperation.before_path ?? ''; const afterPath = rawOperation.afterPath ?? rawOperation.after_path ?? ''; const listPath = rawOperation.listPath ?? rawOperation.list_path ?? ''; const beforeItem = trackerApi.getItemByPath(tracker, beforePath); const afterItem = trackerApi.getItemByPath(tracker, afterPath); const newListItem = buildListItemFromOperation(rawOperation); if (beforeItem?.parentElement) { beforeItem.parentElement.insertBefore(newListItem, beforeItem); copyDonorItemAttributes(newListItem, beforeItem); inheritDestinationItemId(newListItem, beforeItem); return true; } if (afterItem?.parentElement) { afterItem.parentElement.insertBefore(newListItem, afterItem.nextElementSibling); copyDonorItemAttributes(newListItem, afterItem); inheritDestinationItemId(newListItem, afterItem); return true; } const targetList = getListByPath(tracker, listPath); if (!targetList) { return false; } const position = typeof rawOperation.position === 'string' ? rawOperation.position.trim().toLowerCase() : 'append'; if (position === 'prepend' && targetList.firstElementChild) { targetList.insertBefore(newListItem, targetList.firstElementChild); } else { targetList.appendChild(newListItem); } inheritDestinationItemId(newListItem); return true; } /** * Move one existing list item relative to explicit before/after/list anchors. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Target list item. * @param {Object} rawOperation Structural operation payload. * @param {Object} trackerApi List tracker API surface. * @returns {boolean} True when the mutation applied. */ function applyMoveListItemOperation(tracker, listItem, rawOperation, trackerApi) { if (!tracker?.listElement || !listItem || !trackerApi) { return false; } const beforePath = rawOperation.beforePath ?? rawOperation.before_path ?? ''; const afterPath = rawOperation.afterPath ?? rawOperation.after_path ?? ''; const listPath = rawOperation.listPath ?? rawOperation.list_path ?? ''; const beforeItem = trackerApi.getItemByPath(tracker, beforePath); const afterItem = trackerApi.getItemByPath(tracker, afterPath); const oldParentList = listItem.parentElement; let didApply = false; if (beforeItem && beforeItem !== listItem && !listItem.contains(beforeItem)) { beforeItem.parentElement.insertBefore(listItem, beforeItem); copyDonorItemAttributes(listItem, beforeItem); inheritDestinationItemId(listItem, beforeItem); didApply = true; } else if (afterItem && afterItem !== listItem && !listItem.contains(afterItem)) { afterItem.parentElement.insertBefore(listItem, afterItem.nextElementSibling); copyDonorItemAttributes(listItem, afterItem); inheritDestinationItemId(listItem, afterItem); didApply = true; } else if (typeof listPath === 'string') { const targetList = getListByPath(tracker, listPath); const ownerItem = targetList?.parentElement?.tagName === 'LI' ? targetList.parentElement : null; if (targetList && ownerItem !== listItem && !listItem.contains(ownerItem || null)) { const position = typeof rawOperation.position === 'string' ? rawOperation.position.trim().toLowerCase() : 'append'; if (position === 'prepend' && targetList.firstElementChild) { targetList.insertBefore(listItem, targetList.firstElementChild); } else { targetList.appendChild(listItem); } inheritDestinationItemId(listItem); didApply = true; } } if (didApply) { cleanupEmptyAncestorLists(tracker, oldParentList); } return didApply; } /** * Resolve one tracked list element from a list-path payload. * * The root list lives at the empty path. Nested lists are addressed by the * tree path of the parent item that owns that child list. * * @param {Object|null} tracker Active list tracker. * @param {string|Array} pathValue Root-empty list path or parent-item path. * @returns {HTMLElement|null} Matching list element. */ function getListByPath(tracker, pathValue) { if (!tracker?.listElement) { return null; } if ( pathValue === '' || pathValue === null || typeof pathValue === 'undefined' || (Array.isArray(pathValue) && !pathValue.length) || (typeof pathValue === 'string' && !pathValue.trim()) ) { return tracker.listElement; } const parentItem = ListBlockTracker.getItemByPath(tracker, pathValue); return parentItem ? getDirectChildList(parentItem) : null; } /** * Normalize one requested list-type value to a DOM tag name. * * @param {*} value Candidate list-type value. * @returns {string} `OL`, `UL`, or an empty string. */ function normalizeListTypeTagName(value) { if (value === true) { return 'OL'; } if (value === false) { return 'UL'; } const normalized = String(value || '').trim().toLowerCase(); if (normalized === 'ordered' || normalized === 'ol' || normalized === 'true') { return 'OL'; } if (normalized === 'unordered' || normalized === 'ul' || normalized === 'false') { return 'UL'; } return ''; } const ListBlockTracker = { active: null, /** * Initialize tracker for a list element * @param {HTMLElement} listElement - The UL or OL element * @param {Object} originalBlock - The complete WordPress block structure */ init(listElement, originalBlock = {}) { if (!createBlock || !serializeBlocks) { console.error('wp.blocks not available'); } const tracker = { listElement, originalBlock: JSON.parse(JSON.stringify(originalBlock)), uuidMap: new Map(), // uuid -> {type, attrs, element} domMap: new WeakMap(), // element -> uuid runtimeUuidMap: new Map(), // runtimeUuid -> {type, element} runtimeDomMap: new WeakMap() // element -> runtimeUuid }; // Attach tracker to element to avoid singleton issues listElement._mwpListTracker = tracker; // Build UUID tracking from DOM and original block structure this.buildFromDOM(tracker, listElement, originalBlock); this.active = tracker; return tracker; }, /** * Build UUID mappings from DOM and original block structure * Assigns UUIDs to all lists and list items, mapping to their original attrs */ buildFromDOM(tracker, listElement, originalBlock) { const previousEntries = tracker.uuidMap instanceof Map ? new Map(tracker.uuidMap) : new Map(); const previousRuntimeEntries = tracker.runtimeUuidMap instanceof Map ? new Map(tracker.runtimeUuidMap) : new Map(); tracker.uuidMap.clear(); tracker.domMap = new WeakMap(); tracker.runtimeUuidMap.clear(); tracker.runtimeDomMap = new WeakMap(); tracker.listElement = listElement; this.syncEditableTextSurfaces(listElement); this.registerRuntimeIdentity( tracker, listElement, 'list', previousRuntimeEntries ); // Assign UUID to root list and map to original attrs const rootUuid = this.getOrCreateUuid(listElement, 'list'); const previousRootEntry = previousEntries.get(rootUuid); tracker.uuidMap.set(rootUuid, { type: 'list', attrs: previousRootEntry?.attrs ? cloneListAttrs(previousRootEntry.attrs, true) : cloneListAttrs(originalBlock.attrs || {}, true), element: listElement }); tracker.domMap.set(listElement, rootUuid); // Recursively process list structure this.processListRecursive( tracker, listElement, originalBlock.innerBlocks || [], previousEntries, previousRuntimeEntries ); }, /** * Recursively process list items and nested lists, assigning UUIDs */ processListRecursive( tracker, listElement, originalItems, previousEntries = new Map(), previousRuntimeEntries = new Map() ) { const items = getDirectListItems(listElement); items.forEach((li, index) => { this.syncEditableTextSurfaces(li); const originalItem = originalItems[index] || {}; this.registerRuntimeIdentity( tracker, li, 'item', previousRuntimeEntries ); const itemUuid = this.getOrCreateUuid(li, 'item'); const previousItemEntry = previousEntries.get(itemUuid); // Map UUID to original item attrs tracker.uuidMap.set(itemUuid, { type: 'item', attrs: previousItemEntry?.attrs ? JSON.parse(JSON.stringify(previousItemEntry.attrs || {})) : JSON.parse(JSON.stringify(originalItem.attrs || {})), element: li }); tracker.domMap.set(li, itemUuid); // Handle nested lists const nestedList = getDirectChildList(li); if (nestedList) { const originalNested = originalItem.innerBlocks?.[0] || {}; this.registerRuntimeIdentity( tracker, nestedList, 'list', previousRuntimeEntries ); const nestedUuid = this.getOrCreateUuid(nestedList, 'list'); const previousNestedEntry = previousEntries.get(nestedUuid); // Map nested list attrs without plugin UUID ownership. // The tracker still needs a temporary DOM identity for list // editing, but persisting mwpSfeUuid* on nested lists would // fracture one logical list into multiple save/history roots. tracker.uuidMap.set(nestedUuid, { type: 'list', attrs: previousNestedEntry?.attrs ? cloneListAttrs(previousNestedEntry.attrs, false) : cloneListAttrs(originalNested.attrs || {}, false), element: nestedList }); tracker.domMap.set(nestedList, nestedUuid); // Recurse into nested list this.processListRecursive( tracker, nestedList, originalNested.innerBlocks || [], previousEntries, previousRuntimeEntries ); } }); }, /** * Register one list or list-item runtime identity on the tracker. * * @param {Object} tracker Active list tracker. * @param {Element} element Live list or list-item element. * @param {string} type Supported runtime identity type. * @param {Map} previousRuntimeEntries Previous runtime entry map. * @returns {string} Resolved runtime UUID. */ registerRuntimeIdentity( tracker, element, type, previousRuntimeEntries = new Map() ) { const runtimeUuid = this.getOrCreateRuntimeUuid( tracker, element, type, previousRuntimeEntries ); if (!runtimeUuid) { return ''; } tracker.runtimeUuidMap.set(runtimeUuid, { type, element, }); tracker.runtimeDomMap.set(element, runtimeUuid); return runtimeUuid; }, /** * Return whether one runtime UUID is already owned by a different element. * * Native contenteditable list splitting can clone DOM attributes from the * source item into the newly created sibling. When that happens, the new * runtime UUID must be reseeded so each live cursor target stays unique. * * @param {Object} tracker Active list tracker. * @param {string} runtimeUuid Candidate runtime UUID. * @param {Element|null} element Element requesting that UUID. * @returns {boolean} True when the UUID belongs elsewhere. */ isRuntimeUuidClaimedByDifferentElement(tracker, runtimeUuid, element) { const normalizedRuntimeUuid = normalizeRuntimeUuid(runtimeUuid); if (!tracker?.runtimeUuidMap || !normalizedRuntimeUuid) { return false; } const existingEntry = tracker.runtimeUuidMap.get(normalizedRuntimeUuid); return !!(existingEntry?.element && existingEntry.element !== element); }, /** * Get the inherited structural ID for an element or seed it from the * runtime UUID when it does not exist yet. * * Structural IDs may be intentionally copied by native list splitting so * related items can retain style inheritance. They are not treated as * unique runtime cursor identifiers. */ getOrCreateUuid(element, type) { const attrName = type === 'list' ? LIST_ID_ATTR : LIST_ITEM_ID_ATTR; let uuid = element.getAttribute(attrName); if (!uuid) { uuid = normalizeRuntimeUuid( element.getAttribute( type === 'list' ? LIST_RUNTIME_UUID_ATTR : LIST_ITEM_RUNTIME_UUID_ATTR ) ) || this.generateTempUuid(); element.setAttribute(attrName, uuid); } return uuid; }, /** * Return the existing runtime UUID for one element or create one. * * Caller-supplied runtime UUIDs win for newly created items/lists. When an * element already belongs to the previous tracker build, its existing * runtime UUID is preserved so API references remain stable across rebuilds. * * @param {Object} tracker Active list tracker. * @param {Element} element Live list or list-item element. * @param {string} type Supported runtime identity type. * @param {Map} previousRuntimeEntries Previous runtime entry map. * @returns {string} Session-scoped runtime UUID. */ getOrCreateRuntimeUuid( tracker, element, type, previousRuntimeEntries = new Map() ) { const attrName = getRuntimeUuidAttributeName(type); if (!attrName || !isRuntimeIdentityElement(element, type)) { return ''; } let runtimeUuid = normalizeRuntimeUuid(element.getAttribute(attrName)); if (!runtimeUuid) { for (const [candidateUuid, entry] of previousRuntimeEntries.entries()) { if (entry?.element === element && entry.type === type) { runtimeUuid = normalizeRuntimeUuid(candidateUuid); break; } } } if (this.isRuntimeUuidClaimedByDifferentElement(tracker, runtimeUuid, element)) { runtimeUuid = ''; } if (!runtimeUuid) { runtimeUuid = this.generateTempUuid(); } element.setAttribute(attrName, runtimeUuid); return runtimeUuid; }, /** * Generate a RFC4122 version 4 UUID */ generateTempUuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }, /** * Return the zero-based tree path for one list item inside the tracked list. * * @param {Object} tracker Active list tracker. * @param {HTMLLIElement} listItem Candidate list item. * @returns {number[]|null} Zero-based indexes, or null on failure. */ getPathIndexesForItem(tracker, listItem) { if ( !tracker?.listElement || !listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI' || !tracker.listElement.contains(listItem) ) { return null; } const indexes = []; let currentItem = listItem; while (currentItem && tracker.listElement.contains(currentItem)) { const parentList = currentItem.parentElement; if (!parentList || (parentList.tagName !== 'UL' && parentList.tagName !== 'OL')) { return null; } const siblingItems = getDirectListItems(parentList); const itemIndex = siblingItems.indexOf(currentItem); if (itemIndex < 0) { return null; } indexes.unshift(itemIndex); const nextItem = parentList.closest('li'); if (!nextItem || !tracker.listElement.contains(nextItem)) { break; } currentItem = nextItem; } return indexes.length ? indexes : null; }, /** * Return the direct list item currently living at one tree path. * * @param {Object} tracker Active list tracker. * @param {string|Array} pathValue Zero-based path value. * @returns {HTMLLIElement|null} Matching list item. */ getItemByPath(tracker, pathValue) { const indexes = normalizePathIndexes(pathValue); if (!tracker?.listElement || !Array.isArray(indexes) || !indexes.length) { return null; } let currentList = tracker.listElement; let currentItem = null; for (let index = 0; index < indexes.length; index++) { const itemIndex = indexes[index]; const siblingItems = getDirectListItems(currentList); currentItem = siblingItems[itemIndex] || null; if (!currentItem) { return null; } if (index === indexes.length - 1) { return currentItem; } currentList = getDirectChildList(currentItem); if (!currentList) { return null; } } return currentItem; }, /** * Resolve one list element back to its public list path. * * The root list is addressed as an empty string. Nested child lists are * addressed by the path of the parent list item that owns them. * * @param {Object|null} tracker Active list tracker. * @param {HTMLElement|null} listElement Candidate list element. * @returns {string} Root-relative list path. */ getPathForList(tracker, listElement) { if (!tracker) tracker = this.active; if (!tracker?.listElement || !listElement || listElement.nodeType !== Node.ELEMENT_NODE) { return ''; } if (listElement === tracker.listElement) { return ''; } const parentItem = ( listElement.parentElement && listElement.parentElement.tagName === 'LI' ) ? listElement.parentElement : null; if (!parentItem) { return ''; } const indexes = this.getPathIndexesForItem(tracker, parentItem); return Array.isArray(indexes) ? indexes.join('_') : ''; }, /** * Return one tracked list item by runtime UUID. * * @param {Object|null} tracker Active list tracker. * @param {string} runtimeUuid Session-scoped item UUID. * @returns {HTMLLIElement|null} Matching list item. */ getItemByRuntimeUuid(tracker, runtimeUuid) { if (!tracker) tracker = this.active; const element = findElementByRuntimeUuid(tracker, runtimeUuid, 'item'); return element && element.tagName === 'LI' ? element : null; }, /** * Return one tracked list by runtime UUID. * * @param {Object|null} tracker Active list tracker. * @param {string} runtimeUuid Session-scoped list UUID. * @returns {HTMLElement|null} Matching list element. */ getListByRuntimeUuid(tracker, runtimeUuid) { if (!tracker) tracker = this.active; const element = findElementByRuntimeUuid(tracker, runtimeUuid, 'list'); return isListElementForRuntimeLookup(element) ? element : null; }, /** * Return the runtime UUID for one tracked list item. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Live list item. * @returns {string} Session-scoped item UUID. */ getRuntimeUuidForItem(tracker, listItem) { if (!tracker) tracker = this.active; if (!tracker?.listElement || !listItem || listItem.tagName !== 'LI') { return ''; } return normalizeRuntimeUuid( listItem.getAttribute(LIST_ITEM_RUNTIME_UUID_ATTR) || tracker.runtimeDomMap?.get(listItem) ); }, /** * Return the runtime UUID for one tracked list. * * @param {Object|null} tracker Active list tracker. * @param {HTMLElement} listElement Live list element. * @returns {string} Session-scoped list UUID. */ getRuntimeUuidForList(tracker, listElement) { if (!tracker) tracker = this.active; if (!tracker?.listElement || !isListElementForRuntimeLookup(listElement)) { return ''; } return normalizeRuntimeUuid( listElement.getAttribute(LIST_RUNTIME_UUID_ATTR) || tracker.runtimeDomMap?.get(listElement) ); }, /** * Parse current DOM list structure to WordPress block format * Uses UUID to preserve original attrs, updates only content and ordered * * @param {HTMLElement} listElement Live UL/OL element. * @param {Object} tracker Active list tracker. * @param {boolean} allowUuidOwnership True only for the outermost * core/list. Nested lists must * serialize without persisted * plugin UUID attrs. */ parseListToBlock(listElement, tracker, allowUuidOwnership = true) { const isOrdered = listElement.tagName === 'OL'; const uuid = listElement.getAttribute(LIST_ID_ATTR); let attrs = { ordered: isOrdered }; if (uuid && tracker && tracker.uuidMap.has(uuid)) { const original = tracker.uuidMap.get(uuid); const originalAttrs = (Array.isArray(original.attrs) || !original.attrs) ? {} : cloneListAttrs(original.attrs, allowUuidOwnership); attrs = { ...originalAttrs, ordered: isOrdered }; } const listItems = Array.from(listElement.children).filter(el => el.tagName === 'LI'); const innerBlocks = listItems.map(li => this.parseListItemToBlock(li, tracker)); return createBlock('core/list', attrs, innerBlocks); }, /** * Parse a list item to WordPress block format * Preserves original attrs if UUID exists */ parseListItemToBlock(liElement, tracker) { const uuid = liElement.getAttribute(LIST_ITEM_ID_ATTR); let attrs = {}; if (uuid && tracker && tracker.uuidMap.has(uuid)) { const original = tracker.uuidMap.get(uuid); attrs = JSON.parse(JSON.stringify(original.attrs || {})); } // Recurse into nested list using the live element before cloning const innerBlocks = []; const nestedListEl = liElement.querySelector(':scope > ul, :scope > ol'); if (nestedListEl) { innerBlocks.push(this.parseListToBlock(nestedListEl, tracker, false)); } const clone = liElement.cloneNode(true); const directSurface = clone.querySelector(`:scope > [${LIST_ITEM_TEXT_ATTR}="1"]`); let content = ''; if (directSurface) { const cleanedSurface = this.cleanElement(directSurface); content = cleanedSurface.innerHTML.trim(); } else { const nestedInClone = clone.querySelector(':scope > ul, :scope > ol'); if (nestedInClone) nestedInClone.remove(); const cleaned = this.cleanElement(clone); content = cleaned.innerHTML.trim(); } return createBlock('core/list-item', { ...attrs, content }, innerBlocks); }, /** * Clean element using ElementPrep * Removes editing artifacts while preserving UUIDs and content */ cleanElement(element) { if (SFE.ElementPrep) { return SFE.ElementPrep.clean(element, { removeIdentity: true, removeControls: true, clone: false // Already working with a clone }); } console.error('ElementPrep not found'); return element; }, /** * Serialize the current list structure to WordPress block format * Preserves all original attrs, updates only content and ordered */ serialize(tracker) { if (!tracker) tracker = this.active; if (!tracker) return null; const block = this.parseListToBlock(tracker.listElement, tracker); return serializeBlocks([block]); }, /** * Update the tracker's element reference * Used when the list element is replaced (e.g., OL to UL conversion) */ updateElement(tracker, newElement) { if (!tracker) tracker = this.active; if (!tracker) return; // Ensure new element keeps the tracker reference newElement._mwpListTracker = tracker; this.syncEditableTextSurfaces(newElement); tracker.listElement = newElement; // Update element references in uuidMap tracker.uuidMap.forEach((value, key) => { if (value.type === 'list') { const newEl = newElement.querySelector(`[${LIST_ID_ATTR}="${key}"]`) || (newElement.getAttribute(LIST_ID_ATTR) === key ? newElement : null); if (newEl) { value.element = newEl; } } else if (value.type === 'item') { const newEl = newElement.querySelector(`[${LIST_ITEM_ID_ATTR}="${key}"]`); if (newEl) { value.element = newEl; } } }); tracker.runtimeUuidMap.forEach((value, key) => { if (value.type === 'list') { const newEl = this.getListByRuntimeUuid(tracker, key); if (newEl) { value.element = newEl; tracker.runtimeDomMap.set(newEl, key); } } else if (value.type === 'item') { const newEl = this.getItemByRuntimeUuid(tracker, key); if (newEl) { value.element = newEl; tracker.runtimeDomMap.set(newEl, key); } } }); }, /** * Return one tree-aware structural snapshot for the active list. * * @param {Object|null} tracker Active list tracker. * @returns {Object|null} Lightweight structure snapshot. */ getStructure(tracker) { if (!tracker) tracker = this.active; if (!tracker?.listElement) { return null; } const buildListNode = (listElement, listPath = '', parentIndexes = []) => ({ listUuid: this.getRuntimeUuidForList(tracker, listElement), listPath, ordered: listElement.tagName === 'OL', items: getDirectListItems(listElement).map((listItem, index) => { const indexes = [ ...parentIndexes, index ]; const pathMeta = buildPathMeta(indexes); const directSurface = ensureDirectItemTextSurface(listItem); const nestedList = getDirectChildList(listItem); return { itemUuid: this.getRuntimeUuidForItem(tracker, listItem), path: pathMeta.path, pathLabel: pathMeta.pathLabel, depth: pathMeta.depth, contentHtml: directSurface ? directSurface.innerHTML.trim() : '', childList: nestedList ? buildListNode(nestedList, pathMeta.path, indexes) : null, }; }), }); return buildListNode(tracker.listElement, '', []); }, /** * Apply one primitive structural list operation against the live tracked * DOM. * * Public API calls are translated into this lower-level operation set by * the shared schema executor so the tracker only needs to understand the * canonical primitive mutation layer. * * Supported primitive kinds: * - `insert_list_item` * - `remove_list_item` * - `move_list_item` * - `indent_list_item` * - `outdent_list_item` * - `update_list_item_text` * - `toggle_list_type` * * @param {Object|null} tracker Active list tracker. * @param {Object} rawOperation Structural operation payload. * @returns {Object|null} Result summary when applied. */ applyOperation(tracker, rawOperation, options = {}) { if (!tracker) tracker = this.active; if (!tracker?.listElement || !rawOperation || typeof rawOperation !== 'object') { return null; } const kind = typeof rawOperation.kind === 'string' ? rawOperation.kind.trim().toLowerCase() : ''; const path = rawOperation.path ?? rawOperation.itemPath ?? rawOperation.item_path ?? ''; const listItem = this.getItemByPath(tracker, path); let didApply = false; if (kind === 'remove_list_item' && listItem) { didApply = applyRemoveListItemOperation(tracker, listItem); } else if (kind === 'indent_list_item' && listItem) { didApply = applyIndentListItemOperation(tracker, listItem); } else if (kind === 'outdent_list_item' && listItem) { didApply = applyOutdentListItemOperation(tracker, listItem); } else if (kind === 'toggle_list_type') { didApply = applyToggleListTypeOperation(tracker, rawOperation, options); } else if (kind === 'update_list_item_text' && listItem) { didApply = applyUpdateListItemTextOperation(listItem, rawOperation); } else if (kind === 'insert_list_item') { didApply = applyInsertListItemOperation(tracker, rawOperation, this); } else if (kind === 'move_list_item' && listItem) { didApply = applyMoveListItemOperation(tracker, listItem, rawOperation, this); } if (!didApply) { return null; } this.syncEditableTextSurfaces(tracker.listElement); this.buildFromDOM(tracker, tracker.listElement, tracker.originalBlock || {}); return { kind, structure: this.getStructure(tracker), }; }, /** * Ensure every list item in one tree has one direct text surface. * * @param {HTMLElement|null} rootElement Candidate list root or list item. * @returns {HTMLElement|null} Normalized root element. */ syncEditableTextSurfaces(rootElement) { if (!rootElement || rootElement.nodeType !== Node.ELEMENT_NODE) { return null; } if (rootElement.tagName === 'LI') { ensureDirectItemTextSurface(rootElement); Array.from(rootElement.children || []) .filter(child => isNestedListNode(child)) .forEach(childList => this.syncEditableTextSurfaces(childList)); return rootElement; } if (rootElement.tagName !== 'UL' && rootElement.tagName !== 'OL') { return rootElement; } Array.from(rootElement.children || []) .filter(child => child.tagName === 'LI') .forEach(li => this.syncEditableTextSurfaces(li)); return rootElement; }, /** * Ensure runtime UUID attributes are unique within one live list tree. * * Native contenteditable list splitting can clone `li` and nested list * attributes verbatim before FrontEdit regains control. This pass reseeds only the * session-scoped runtime UUID attributes so freshly created structures become * distinct live API/editor targets immediately, while leaving `data-item-id` * and `data-list-id` untouched for their separate responsibilities. * * @param {HTMLElement|null} rootElement Candidate list root. * @returns {HTMLElement|null} Normalized root element. */ ensureUniqueRuntimeUuids(rootElement) { if ( !rootElement || rootElement.nodeType !== Node.ELEMENT_NODE || (rootElement.tagName !== 'UL' && rootElement.tagName !== 'OL') ) { return rootElement || null; } const seenListRuntimeUuids = new Set(); const seenItemRuntimeUuids = new Set(); const collectLists = [ rootElement, ...Array.from(rootElement.querySelectorAll('ul, ol')) ]; const collectItems = Array.from(rootElement.querySelectorAll('li')); collectLists.forEach(listElement => { const runtimeUuid = normalizeRuntimeUuid( listElement.getAttribute(LIST_RUNTIME_UUID_ATTR) ); if (!runtimeUuid) { return; } if (seenListRuntimeUuids.has(runtimeUuid)) { listElement.setAttribute(LIST_RUNTIME_UUID_ATTR, this.generateTempUuid()); return; } seenListRuntimeUuids.add(runtimeUuid); }); collectItems.forEach(listItem => { const runtimeUuid = normalizeRuntimeUuid( listItem.getAttribute(LIST_ITEM_RUNTIME_UUID_ATTR) ); if (!runtimeUuid) { return; } if (seenItemRuntimeUuids.has(runtimeUuid)) { listItem.setAttribute(LIST_ITEM_RUNTIME_UUID_ATTR, this.generateTempUuid()); return; } seenItemRuntimeUuids.add(runtimeUuid); }); return rootElement; }, /** * Ensure one live list tree has the required structural IDs and runtime UUIDs. * * On first editor activation, list elements may not yet have either identity * attribute family. Seed both from one generated UUID per element so the * initial history snapshot captures stable targeting data. Later native list * splits may intentionally copy the structural IDs; in those cases this pass * preserves the structural IDs and only reseeds duplicated runtime UUIDs. * * @param {HTMLElement|null} rootElement Candidate list root. * @returns {HTMLElement|null} Normalized root element. */ ensureIdentityAttributes(rootElement) { if ( !rootElement || rootElement.nodeType !== Node.ELEMENT_NODE || (rootElement.tagName !== 'UL' && rootElement.tagName !== 'OL') ) { return rootElement || null; } const syncIdentityPair = (element, structuralAttrName, runtimeAttrName) => { if (!element || element.nodeType !== Node.ELEMENT_NODE) { return; } let structuralId = normalizeRuntimeUuid( element.getAttribute(structuralAttrName) ); let runtimeUuid = normalizeRuntimeUuid( element.getAttribute(runtimeAttrName) ); if (!structuralId && !runtimeUuid) { runtimeUuid = this.generateTempUuid(); structuralId = runtimeUuid; } else if (!runtimeUuid) { runtimeUuid = structuralId; } else if (!structuralId) { structuralId = runtimeUuid; } element.setAttribute(structuralAttrName, structuralId); element.setAttribute(runtimeAttrName, runtimeUuid); }; syncIdentityPair(rootElement, LIST_ID_ATTR, LIST_RUNTIME_UUID_ATTR); Array.from(rootElement.querySelectorAll('ul, ol')).forEach(listElement => { syncIdentityPair(listElement, LIST_ID_ATTR, LIST_RUNTIME_UUID_ATTR); }); Array.from(rootElement.querySelectorAll('li')).forEach(listItem => { syncIdentityPair(listItem, LIST_ITEM_ID_ATTR, LIST_ITEM_RUNTIME_UUID_ATTR); }); return this.ensureUniqueRuntimeUuids(rootElement); }, getDirectItemTextSurface, normalizePathIndexes, buildPathMeta, getRuntimeUuidAttributeName, getListRuntimeUuidAttributeName() { return LIST_RUNTIME_UUID_ATTR; }, getListItemRuntimeUuidAttributeName() { return LIST_ITEM_RUNTIME_UUID_ATTR; }, getListIdAttributeName() { return LIST_ID_ATTR; }, getListItemIdAttributeName() { return LIST_ITEM_ID_ATTR; }, /** * Destroy tracker and clean up */ destroy(tracker) { if (!tracker) tracker = this.active; if (!tracker) return; tracker.uuidMap.clear(); tracker.runtimeUuidMap.clear(); if (tracker.listElement) { delete tracker.listElement._mwpListTracker; } if (this.active === tracker) this.active = null; } }; /** * Return whether one element is a live list node for runtime lookup helpers. * * @param {Element|null} element Candidate DOM element. * @returns {boolean} True when the element is `UL` or `OL`. */ function isListElementForRuntimeLookup(element) { return !!( element && element.nodeType === Node.ELEMENT_NODE && (element.tagName === 'UL' || element.tagName === 'OL') ); } // Expose globally SFE.ListBlockTracker = ListBlockTracker; })(); Eric Schmid, Author at Luxe Designs - Page 18 of 25

Maximize your winnings at Instant Withdrawal Casino Canada: Real money games and secure payment



In the dynamic world of online gaming, securing your winnings swiftly is paramount, especially at instant withdrawal casinos in Canada that offer instant casino options for players seeking excitement and quick payouts. These platforms not only offer a vast selection of real money games but also ensure fast and secure payment options, making your gaming experience both thrilling and worry-free. This article will explore how to maximize your winnings while enjoying the benefits of these premier casinos.

A focused look at registration and player value

When considering online casinos, the registration process and the value offered to players are crucial factors. Instant withdrawal casinos in Canada prioritize a seamless registration experience, allowing players to quickly create an account and dive into their gaming adventures. These casinos often provide a rich selection of games, including over 3,000 slot options, ensuring that players find something that suits their tastes. Additionally, attractive welcome bonuses, such as up to $5,000 and 300 free spins, enhance the initial player experience, setting the stage for potential big wins.

Moreover, these casinos ensure that players get the most value from their gaming sessions through ongoing promotions and rewards. By prioritizing player satisfaction and providing a user-friendly interface, instant withdrawal casinos are making online gambling more accessible and enjoyable for everyone.

How to start playing at an instant withdrawal casino

Getting started with instant withdrawal casinos is straightforward. Here’s a step-by-step guide to help you begin your exciting journey:

  1. Create an Account: Sign up by providing essential information to establish your player profile.
  2. Verify Your Details: Complete necessary KYC checks to ensure a secure gaming environment.
  3. Make a Deposit: Choose a secure payment method, such as Interac, crypto, or e-wallets, and fund your account.
  4. Select Your Game: Browse the extensive game library, featuring various real money options, and pick your favorite.
  5. Claim Your Welcome Bonus: Take advantage of attractive bonuses to boost your playing power.
  6. Start Playing: Immerse yourself in the gaming experience and enjoy the thrill of winning!
  • Quick account setup for immediate access to games.
  • Fast verification ensures a safe environment.
  • Diverse payment options for easy transactions.

Practical details for playing at Canadian casinos

At instant withdrawal casinos, the practical aspects of gaming significantly enhance the overall experience. Players can enjoy a wide array of games, including slot machines, table games, and live dealer options, providing something for everyone. The sheer volume of available games, often exceeding 3,000 titles, guarantees that players will never run out of options, whether they prefer classic slots or the latest releases. Additionally, the user-friendly interface streamlines navigation, making it easier to find and play your favorite games.

  • Access to over 3,000 diverse slot games.
  • High betting limits for more significant potential wins.
  • Availability of engaging live dealer games for an immersive experience.
  • Regular updates and new game releases to keep things fresh.

With such variety, players can strategize and explore different game types while maximizing their chances of winning. It’s not just about luck; understanding the games and utilizing bonuses effectively can lead to significant payouts.

Key benefits of instant withdrawal casinos

Choosing to play at instant withdrawal casinos offers numerous advantages that enhance the gambling experience. These casinos are designed with player satisfaction in mind, ensuring both convenience and excitement.

  • Fast payouts: Enjoy swift access to your winnings with instant withdrawals.
  • Multiple payment methods: Choose from various secure options, including e-wallets and cryptocurrencies, for hassle-free transactions.
  • Generous welcome bonuses: Start your gaming journey strong with bonuses up to $5,000 and additional free spins.
  • High limits: Many casinos offer high betting limits for players looking to maximize potential winnings.

These features make instant withdrawal casinos an appealing choice for both novice and veteran players alike, enhancing the overall gaming experience while ensuring that players feel valued and secure.

Trust and security at instant withdrawal casinos

Trust and security are vital components when selecting an online casino. Reputable instant withdrawal casinos are licensed and regulated, ensuring compliance with industry standards. This licensing provides reassurance to players that their sensitive information is protected, and fair play is enforced. Additionally, many casinos conduct thorough KYC (Know Your Customer) checks, verifying player identities upfront to mitigate fraud risks and secure transactions.

Utilizing cutting-edge encryption technologies, these casinos foster a safe gaming environment where players can focus on enjoying their favorite games without worrying about security issues. Keeping player data safe is a top priority, ensuring that your gaming experience is both enjoyable and secure.

  • Regulated and licensed operations.
  • Reliable encryption methods to safeguard personal data.
  • Transparent policies regarding player transactions.

Why choose instant withdrawal casinos in Canada

As the online casino landscape continues to evolve, instant withdrawal casinos stand out for their commitment to player satisfaction and secure gaming experiences. These casinos not only offer a vast range of games and rapid payouts but also ensure that players can trust their platforms due to stringent security measures. With enticing bonuses and a user-friendly interface, these casinos provide the perfect environment for enjoying real money games.

Ultimately, the combination of fast withdrawals, generous bonuses, and a wide selection of engaging games makes instant withdrawal casinos an excellent choice for anyone looking to elevate their online gaming experience. So, why wait? Sign up today and start maximizing your winnings!

Begin your gaming journey at Nagad88 Bangladesh: essential tips for new players



Are you ready to embark on an exciting gaming adventure? Online casinos like Nagad88 offer a thrilling experience for both new and seasoned players alike, and you can discover even more exciting options through the nagad88 apk . With a wide variety of games, user-friendly features, and attractive bonuses, starting your journey in the world of online gaming has never been easier. In this article, we’ll explore essential tips for newcomers to ensure a smooth and enjoyable experience at Nagad88 Bangladesh.

What the first visit should reveal about Nagad88 Bangladesh

When you first visit Nagad88, you’ll quickly discover a vibrant virtual space filled with enticing gaming options tailored for players in Bangladesh. From over 1000 slots to live dealer games, the platform caters to diverse preferences. The design emphasizes user experience, making navigation intuitive for newcomers. Furthermore, clear information about promotions, payment methods, and customer support will be readily available. Familiarity with these features will significantly enhance your initial gaming experience.

Additionally, first-time players will appreciate the welcoming atmosphere, with enticing bonuses and promotions that are designed to help you get started. A quick glance will show how easy it is to deposit and withdraw funds using popular local payment methods like bKash and Nagad, ensuring a smooth financial transaction process. Understanding these elements sets a solid foundation for your gaming journey.

How to get started at Nagad88 Bangladesh

Getting started at Nagad88 is a straightforward process. Follow these simple steps to dive into your gaming adventure:

  1. Create an Account: Sign up by providing your basic details, including your email and preferred password.
  2. Verify Your Details: Ensure that your account is secure by verifying your email and personal information.
  3. Make a Deposit: Choose your payment method, with options like bKash or Nagad, and deposit a minimum of 15 BDT to start playing.
  4. Select Your Game: Browse the extensive collection of games, including slots and live dealer options, to find what suits you best.
  5. Start Playing: Enjoy your chosen game and have fun. Don’t forget to keep track of your bankroll and set limits!
  • Quick and easy account creation process.
  • Immediate access to bonuses upon depositing.
  • Variety of games available right from the start.

Practical insights for a seamless gaming experience

One of the standout features of Nagad88 is its user-friendly interface, ensuring that even newcomers can navigate with ease. The platform’s design minimizes confusion, allowing you to focus on playing your favorite games. Players can enjoy a wide range of slots and live dealer experiences, which are complemented by thrilling features and realistic graphics. Moreover, the platform provides 24/7 live chat support, available in both Bengali and English, ensuring that any questions or concerns can be promptly addressed.

  • Access to over 1000 slots and various live dealer games.
  • 24/7 customer support available for assistance.
  • Quick withdrawal speeds starting from just 15 minutes.

The variety of payment options, including bKash, Nagad, Rocket, and Upay, makes it incredibly convenient to manage your funds. Knowing that you can deposit and withdraw swiftly allows players to focus on enjoying their gaming experience without worry.

Key benefits of playing at Nagad88 Bangladesh

Nagad88 offers several benefits that make it an appealing choice for players in Bangladesh. Their attractive welcome bonus of 100% up to ৳20,000 enables newcomers to explore a wider variety of games while expanding their initial bankroll. This bonus facilitates a more engaging experience right from the beginning, allowing players to try out different games without the fear of losing their initial investment.

  • Generous welcome bonus to kickstart your gaming journey.
  • Extensive collection of games to suit all preferences.
  • Fast and secure payment methods tailored for Bangladeshi players.
  • Accessible customer support to assist with any issues.

These advantages highlight why Nagad88 stands out among online casinos, making it a strong contender for anyone looking to start their gaming journey.

Trust and security at Nagad88 Bangladesh

Trust and security are paramount in the online gaming world, and Nagad88 takes these concerns seriously. The platform operates under a license from Curacao, ensuring that players are protected while enjoying their gaming experience. This licensing means that Nagad88 adheres to stringent regulations, providing a safe and fair gaming environment for all players.

Additionally, all transactions are processed using advanced encryption technology, ensuring that your personal and financial information remains secure. Knowing this gives players peace of mind, allowing them to focus on enjoying the games without worrying about security issues.

Why choose Nagad88 Bangladesh?

Choosing Nagad88 for your online gaming experience means opting for a platform that prioritizes player satisfaction, security, and a vast array of gaming options. With engaging features, swift payouts, and local payment methods, players will find it easy to both deposit and withdraw their earnings. The generous welcome bonus serves as an excellent incentive to begin your journey, allowing for exploration without the pressure of immediate risk.

Whether you’re interested in slots, live dealer games, or sports betting, Nagad88 offers a comprehensive space to cater to all gaming preferences. As you step into the vibrant world of online gaming, Nagad88 provides the perfect launchpad for an exciting adventure.

Rocket Casino Australia mobile app review: play your favorite pokies anytime, anywhere



Welcome to the exciting world of online gaming where Rocket Casino Australia stands out as a premier destination for enthusiasts. This platform specializes in providing an extensive selection of pokies and real money games, allowing Australian players to immerse themselves in thrilling gameplay, including options like casino rocket that elevate the gaming experience. With its mobile-friendly interface, players can enjoy their favorite games anytime, anywhere, making it an ideal choice for those who love to game on the go.

How beginners can approach Rocket Casino Australia

For newcomers diving into the world of online casinos, understanding the basics is crucial. Rocket Casino Australia offers a streamlined experience that caters specifically to beginners. The website is easy to navigate, ensuring that even those unfamiliar with online casinos can find their way around. Whether you’re interested in pokies or other real money games, there’s something for everyone.

What sets Rocket Casino apart is its generous welcome bonus, offering new players an enticing entry into the gaming experience. Additionally, the option of fast withdrawals and a variety of payment methods, including cryptocurrency, adds to the convenience, making the onboarding process seamless for new players.

How to get started

Getting started at Rocket Casino Australia is a straightforward process that ensures you can quickly jump into the action. Follow these simple steps to begin your gaming adventure:

  1. Create an Account: Visit the Rocket Casino website and sign up by providing your details.
  2. Verify Your Details: Confirm your identity as part of the registration process for a secure experience.
  3. Make a Deposit: Choose your preferred payment method, with a minimum deposit as low as A$15.
  4. Claim Your Welcome Bonus: Take advantage of the generous welcome offer of 150% up to A$1,575 and receive 150 free spins on Big Bass Splash.
  5. Select Your Game: Browse through the extensive library of over 3,000 games and pick your favorite pokies.
  6. Start Playing: Once your account is funded, dive into the exciting world of pokies and real money games!
  • Quick setup allows you to start playing in minutes.
  • Generous bonuses enhance your gaming experience.
  • A wide variety of games ensures there’s something for everyone.

Practical details for enjoying Rocket Casino Australia

As you embark on your gaming journey with Rocket Casino, it’s essential to understand the features that enhance your experience. The mobile casino platform is designed for optimal performance, allowing players to enjoy their favorite pokies on a variety of devices. The interface is user-friendly, making it easy to navigate through games and promotions.

Rocket Casino also prioritizes fast withdrawals, ensuring that once your request is approved, your funds are processed instantly. The minimum withdrawal amount starts from A$30, making it accessible for most players. Moreover, the casino supports multiple payment options, including popular cryptocurrencies, enhancing convenience and security for Australian players.

  • Seamless mobile experience for gaming on the go.
  • Instant withdrawal processing for faster access to winnings.
  • A broad range of secure payment methods, including crypto.

The extensive selection of games includes not just traditional pokies but also innovative new titles, catering to all preferences. With new games being added regularly, players can always find fresh options to keep their experience exciting.

Key benefits of playing at Rocket Casino Australia

Rocket Casino Australia offers a range of benefits that enhance the gaming experience for players. One of the key attractions is the impressive library of games, which exceeds 3,000 options. This extensive selection ensures that players can always find something that suits their tastes, whether they prefer classic slots or the latest releases.

  • 150% welcome bonus up to A$1,575 boosts your initial gameplay.
  • 150 free spins on Big Bass Splash provide additional chances to win.
  • An instant withdrawal process ensures quick access to your winnings.
  • A wide range of real money games keeps the entertainment flowing.

Beyond the immediate benefits, Rocket Casino also emphasizes player safety and responsible gaming, providing tools and resources for maintaining a fun and secure gaming environment.

Trust and security at Rocket Casino Australia

When it comes to online gaming, trust and security are paramount. Rocket Casino Australia is licensed and regulated, ensuring a safe gaming environment for all players. The casino employs advanced security measures, including encryption technology, to protect players’ sensitive information and transactions. This dedication to security allows you to focus on enjoying your gaming experience without worrying about safety concerns.

Additionally, Rocket Casino promotes responsible gaming practices by providing resources for players to manage their gaming habits effectively. With tools such as deposit limits and self-exclusion options, players can enjoy their time at the casino while ensuring they stay within their means.

Why choose Rocket Casino Australia

Choosing Rocket Casino Australia for your online gaming experience offers a wealth of advantages. The combination of a vast selection of pokies, generous bonuses, and a user-friendly mobile platform creates an engaging environment for both new and seasoned players. With instant withdrawals and a wide range of payment options, including cryptocurrency, Rocket Casino ensures a convenient and enjoyable gaming experience.

Whether you’re looking to spin the reels on your favorite pokies or explore new real money games, Rocket Casino Australia serves as an excellent choice for gaming enthusiasts. With so much on offer, it’s time to dive into the action and experience the excitement for yourself!

Online Pokies NZ 2026 review: Why this offshore casino stands out for New Zealand



As New Zealand continues to embrace the digital age, online gambling has gained substantial traction, with online pokies becoming a popular choice among players. The year 2026 brings exciting developments in the world of offshore casinos, particularly for Kiwis seeking safe and thrilling gaming experiences, such as the ever-popular nz online casino , which offers unique features and bonuses. Whether you’re a seasoned player or just stepping into the realm of online gaming, understanding what sets these platforms apart is essential for enjoying your time and maximizing your potential winnings.

How new players can read the key casino signals

When diving into the online casino world, understanding the key signals that indicate a safe and reliable gaming environment is crucial. Players should look for signs that demonstrate a platform’s trustworthiness, quality, and entertaining offerings. These signals include licensing information, customer support availability, and the variety of games offered. With emerging offshore casinos, New Zealand players particularly benefit from bonuses tailored to enhance their gaming experience, as well as a wide selection of online pokies that cater to various preferences.

Furthermore, the user interface plays a significant role in assessing an online casino. A well-designed website or mobile platform ensures that players can navigate easily and enjoy their gaming sessions without unnecessary hiccups. Recognizing these aspects will empower players to make informed choices when selecting an online casino.

How to get started with online pokies

Getting started with online pokies is straightforward, and following these steps will ensure a smooth experience:

  1. Create an Account: Begin by visiting the offshore casino’s website and registering for an account.
  2. Verify Your Details: Complete the necessary verification process to confirm your identity and age.
  3. Make a Deposit: Choose your preferred payment method and deposit funds into your casino account.
  4. Select Your Game: Browse the extensive collection of online pokies and choose one that interests you.
  5. Start Playing: Enjoy your gaming session and take advantage of any welcome bonuses available.
  • Seamless account creation process
  • Quick verification allows for faster gameplay
  • Diverse payment options cater to various preferences

Practical details about online gambling in New Zealand

New Zealand players can enjoy a wealth of online pokies options with significant features that enhance the overall gambling experience. Many offshore casinos offer a welcome bonus of up to NZ$1,600+, making it an excellent opportunity to boost your initial bankroll. Moreover, the instant crypto withdrawals available at select casinos ensure that you can access your winnings without unnecessary delays, enhancing the overall convenience of online gambling.

Another crucial aspect is the mobile compatibility of these platforms. The rise of mobile gaming allows players to enjoy pokies anytime, anywhere, ensuring that the thrill of gambling is always within reach. Payments in NZD further simplify the process for New Zealand players, eliminating the hassle of currency conversion and making transactions straightforward.

  • Welcome bonuses enhance gameplay
  • Instant withdrawals through cryptocurrency
  • Mobile-friendly platforms offer flexibility

These practical details highlight the advantages of engaging with offshore casinos, particularly for Kiwis seeking an exciting gaming backdrop.

Key benefits of offshore casinos for Kiwis

Offshore casinos provide several advantages for New Zealand players, making them a compelling choice for online gambling. These benefits not only enhance the gaming experience but also promote a sense of security and trust. Understanding these advantages can help players make confident and informed decisions.

  • Diverse Game Selection: A wide array of pokies and other games provides endless entertainment options.
  • Attractive Promotions: Regular bonuses and promotions keep the gaming experience fresh and rewarding.
  • Reliable Customer Support: 24/7 support ensures that players can get help whenever needed.
  • Safe and Secure Transactions: Advanced encryption technology safeguards players’ financial information and gameplay.

The combination of these benefits reinforces the appeal of offshore casinos and their commitment to delivering a trustworthy and enjoyable gaming environment.

Trust and security in online gambling

When engaging with offshore casinos, trust and security are paramount. Reputable platforms employ robust security measures, including SSL encryption, which protects sensitive customer data during transactions. Additionally, reliable casinos are licensed by respected gaming authorities, providing players with peace of mind regarding their safety and fair play. Regulatory compliance ensures that these platforms adhere to established standards, creating a fair gambling ecosystem.

Players should remain vigilant and choose casinos that demonstrate transparency regarding their licensing and security protocols. This diligence helps safeguard their funds and personal information, ultimately enhancing their online gaming experience.

  • Transparency about licensing ensures compliance
  • SSL encryption protects personal information
  • Reputable casinos maintain fair gaming practices

Why choose offshore online pokies casinos

For New Zealand players, selecting an offshore online pokies casino offers numerous advantages aimed at enhancing the gaming experience. With a broader variety of games, lucrative bonuses, and dependable customer support, Kiwis can fully immerse themselves in the exciting world of online gaming. The ability to make easy deposits and withdrawals using NZD and various methods adds further convenience. Coupled with high standards of security, players can enjoy their favorite pokies without the worry of compromising their safety.

As the online gambling landscape continues to evolve in 2026, it is essential for players to stay informed and select platforms that prioritize their needs. Understanding the benefits and features that distinguish these offshore casinos can lead to a fulfilling and enjoyable gaming journey.

Dive into the world of Best Online Pokies Australia: Real money slots and quick



Australia’s vibrant online casino scene continues to grow, offering players an exciting mix of real money pokies. With an array of games, fast withdrawals, and enticing bonuses, exploring these digital establishments can be an exhilarating experience. As of 2026, players can find the best licensed online pokies sites in Australia that promise secure payments and effortless gameplay, including options like the best australian online casino which enhances the overall gaming experience. Let’s dive into this thrilling realm where chance meets opportunity, and discover what makes online pokies a favorite among gamers in Australia.

A clear starting point for casino

Online casinos have transformed the way players engage with their favorite games. With the advent of technology, accessing a wide selection of pokies has never been easier. Australian online pokies sites are designed to cater to both seasoned players and newcomers, offering a seamless blend of entertainment and potential rewards. Players can immerse themselves in themes that range from adventure to mythology, all while enjoying the thrill of potential payouts. In essence, this is not just about spinning reels; it is about creating an engaging experience that resonates with players.

Many online casinos ensure they are licensed and regulated, providing a sense of safety and trust. As you engage with various platforms, it’s important to look out for those that offer favorable terms, including substantial welcome bonuses and reliable payment options like PayID and Neosurf. This enhances the overall gaming experience while ensuring players feel secure while they enjoy their favorite slots.

How to get started with online pokies

Setting up your journey in the world of online pokies is straightforward, allowing you to focus on gameplay rather than technicalities. Here’s a step-by-step guide to help you get started:

  1. Create an Account: Registering with a licensed online casino is your first step. Fill out the required details and agree to the terms.
  2. Verify Your Details: Most platforms will ask for verification. This ensures that your information is secure and helps in smooth withdrawals.
  3. Make a Deposit: Choose from various payment methods, including PayID or Neosurf, to fund your account comfortably.
  4. Select Your Game: Browse through a wide selection of pokies and choose one that aligns with your preferences.
  5. Start Playing: Place your bets and enjoy spinning the reels. Always remember to gamble responsibly.
  • Easy registration process with quick account setup.
  • Variety of payment options for seamless transactions.
  • Instant access to a plethora of pokies once your account is funded.

Practical details for enjoying online pokies

Engaging with online pokies in Australia not only provides entertainment but also comes with various benefits. One such advantage is the impressive range of welcome bonuses available in 2026. New players often encounter enticing offers, such as a 450% bonus up to 5,000 AUD and additional free spins. Such promotions are designed to boost your bankroll, allowing for more extended gameplay and increased chances of winning. Furthermore, the withdrawal processes are swift, with options for cryptocurrency allowing you to access your winnings within 12 to 24 hours. Card withdrawals typically take between 1 to 3 days, ensuring that your money is easily accessible.

  • Significant welcome bonuses that enhance your initial deposit.
  • Fast withdrawal options ensuring you get your winnings quickly.
  • A wide range of games, appealing to diverse player preferences.

The convenience of mobile compatibility is another key feature of online pokies. Most platforms are optimized for mobile devices, meaning you can enjoy your favorite games from anywhere, whether at home or on the go. Ease of access makes the experience even more enjoyable, focusing on the excitement of gaming rather than logistical challenges.

Key benefits of playing online pokies

Exploring the world of online pokies brings multiple advantages that enhance the gaming experience. Here are some notable benefits that players can enjoy:

  • Variety of Games: Online casinos offer hundreds of different pokies, ensuring players never run out of options to explore.
  • Bonuses and Promotions: Continuous promotions such as reload bonuses, free spins, and loyalty rewards keep players engaged and motivated.
  • Convenience: The ability to play from anywhere at any time without needing to travel to a physical casino adds to the appeal.
  • Secure Transactions: Licensed casinos prioritize security, providing peace of mind regarding your financial transactions.

These benefits contribute to a rich environment that encourages both fun and strategic play, making online pokies an attractive option for many Australians.

Trust and security in online gaming

When engaging with online casinos, security is paramount. Reputable Australian online pokies sites employ advanced encryption technology to safeguard player information and financial transactions. This safeguarding ensures that personal data remains confidential and protected from potential threats. Additionally, choosing licensed casinos means that these platforms adhere to strict regulatory standards, providing a layer of trustworthiness.

Moreover, established casinos offer responsible gambling features, allowing players to set limits on their deposits and playtime. This not only promotes a healthy gaming environment but also ensures that players enjoy their experience without taking unnecessary risks.

  • Advanced encryption technology to protect your data.
  • Regulated platforms for reliable gaming experiences.
  • Responsible gambling options to encourage healthy gaming habits.

Why choose the top online pokies sites

Choosing the right online pokies site can significantly enhance your gaming experience. The best platforms combine reliability with user-friendly interfaces, diverse game selections, and lucrative bonuses. In 2026, it’s crucial to opt for licensed sites that not only offer security but also provide a thrilling gaming atmosphere. This ensures that as you explore different pokies, you can do so confidently, knowing that your experience is catered to your preferences.

In summary, the world of online pokies in Australia presents an engaging and rewarding experience for players. With continuous advancements in technology and an expansive range of games and bonuses, there has never been a better time to dive into this exciting domain. Make informed choices, enjoy the journey, and embrace the thrill of potential winnings as you spin the reels in 2026.

Neosurf Casino Australia: your gateway to the best online pokies and secure banking options



In the vibrant world of online gaming, Neosurf casinos in Australia play a pivotal role by providing players with secure banking options and an extensive selection of online pokies. As the popularity of online gambling grows, Australian players seek platforms that not only offer exciting gaming experiences but also prioritize safety and ease of use, such as australia online casino , which have emerged as reliable options, allowing players to enjoy their favorite games while ensuring their financial transactions are quick and secure.

How new players can read the key casino signals

Understanding the landscape of online casinos is crucial for both newcomers and seasoned players alike. When exploring Neosurf casinos, players should look for specific signals indicating a trustworthy gaming environment. This includes checking the site’s licensing information, game variety, payment options, and customer support availability. Being informed about these key aspects ensures a satisfying and secure gaming experience while maximizing the potential for winnings.

Moreover, it’s essential to pay attention to the various promotions offered, such as welcome bonuses and free spins, which can significantly enhance the initial gaming experience. By choosing a reputable Neosurf casino, players can dive into a world of exciting online pokies while knowing their deposits are handled securely.

How to get started with Neosurf casinos

Getting started at a Neosurf casino is straightforward and user-friendly. Below is a step-by-step guide to help players enjoy their online gaming experience:

  1. Create an Account: Register on the casino site by providing your details, which typically include your name, email address, and date of birth.
  2. Verify Your Details: Complete the verification process to ensure your account is secure and compliant with regulations.
  3. Make a Deposit: Choose Neosurf as your preferred payment method, with minimum deposits starting as low as 10 AUD.
  4. Select Your Game: Browse through a diverse selection of online pokies and other games available at the casino.
  5. Start Playing: Dive into your chosen game and enjoy the thrill of online gambling.
  • Simple account creation process for convenience
  • Fast and secure deposits using Neosurf
  • Wide variety of games to ensure every player finds something they enjoy

Practical details for enjoying online pokies

Neosurf casinos in Australia offer an extensive range of online pokies, making them incredibly appealing to players. Featuring various themes, designs, and game mechanics, these pokies provide hours of entertainment. Players can enjoy popular titles that boast exciting graphics and engaging gameplay. Additionally, many casinos offer exclusive bonuses, such as up to 3,000 AUD plus 200 free spins, making it all the more tempting to join.

Players can also benefit from a fast mobile cashier, allowing them to manage their funds seamlessly from their mobile devices. The flexibility of mobile compatibility means that you can enjoy your favorite games on the go, without sacrificing quality. With the right Neosurf casino, players can immerse themselves in a truly dynamic gaming environment.

  • A wide variety of online pokies, including classic and modern options
  • Exclusive promotions and generous welcome bonuses to boost your initial gaming funds
  • Seamless mobile experience for gaming anytime, anywhere

With so many enticing options and features available, it’s clear why Neosurf casinos are a popular choice for players in Australia.

Key benefits of choosing Neosurf casinos

Opting for Neosurf as a payment method at online casinos comes with numerous advantages. Firstly, it enhances the security of online transactions, as players do not need to share sensitive banking information directly with the casino. Secondly, Neosurf transactions are processed quickly, allowing for instant deposits, which means players can start gaming right away.

  • High level of security to protect player information
  • Quick transaction processing for immediate gameplay
  • User-friendly experience for both new and experienced players
  • Access to exclusive bonuses that can increase your playtime

These benefits make Neosurf casinos a preferred choice for many Australian players seeking both security and excitement.

Trust and security in Neosurf casinos

One of the foremost concerns for players at online casinos is trust and security. Neosurf casinos prioritize player safety by employing advanced encryption technologies, ensuring that all transactions are protected from unauthorized access. Additionally, reputable casinos are licensed by recognized regulatory authorities, further enhancing their credibility. Players can feel confident knowing that their funds and personal data are secure while enjoying their favorite online games.

Moreover, Neosurf’s payment method does not require personal bank details, which adds an extra layer of security. This anonymity appeals to many players who prefer to keep their financial transactions discreet.

Why choose Neosurf casinos in Australia

Choosing a Neosurf casino provides Australian players with an exciting and secure online gaming experience. With the ability to make straightforward deposits and access a myriad of online pokies, players can enjoy a seamless gaming session. The attractive bonuses and promotions offered by these casinos only enhance the overall experience, making it an enjoyable venture.

In conclusion, Neosurf casinos stand out for their commitment to player security, convenience in payment, and an expansive selection of games. Whether you’re a new player looking to explore or an experienced gambler seeking a trustworthy platform, Neosurf casinos in Australia offer everything you need for an engaging and rewarding gaming experience.

Best Online Casino South Africa 2026: your ultimate resource for promotions and game selection



The online casino landscape in South Africa has blossomed, providing players with a multitude of options for exhilarating gaming experiences. In 2026, choosing the right online casino is crucial for enjoying a safe, secure, and rewarding gaming experience, especially when you consider the best online casino options available that cater to various preferences. This article serves as your ultimate resource, highlighting the best features, promotions, and game selections offered by top online casinos in South Africa. Whether you are a new player or a seasoned gambler, understanding the landscape will help you maximize your enjoyment.

How the core features support everyday play

Navigating the world of online casinos can be overwhelming, especially with the plethora of choices available. Key features that enhance everyday play include game selection, promotions, and payment options. A good online casino offers a wide variety of games, including premium slots, table games, and live dealer options, creating a vibrant gaming atmosphere. Furthermore, attractive promotions such as welcome bonuses and free spins encourage engagement and can significantly boost your bankroll.

With fast withdrawals and diverse payment options, players can enjoy a seamless experience when managing their funds. Online casinos that support ZAR payment methods make it easier for South African players to deposit and withdraw money conveniently. Understanding these core features can lead to a more enjoyable and productive gaming experience.

How to get started

Getting started with an online casino in South Africa can be a simple process if you follow the right steps. Here’s a detailed guide to help you get going:

  1. Create an Account: Visit the casino’s website and complete the registration form with your details.
  2. Verify Your Details: Confirm your identity by providing necessary documentation, enhancing security.
  3. Make a Deposit: Choose your preferred payment method, whether it’s an OTT Voucher or Ozow Instant EFT.
  4. Select Your Game: Explore a range of games, including the latest slots and classic table games.
  5. Start Playing: Enjoy your gaming experience, taking advantage of promotions and bonuses available.
  • Quick and easy account creation ensures you can start playing sooner.
  • Verification enhances your security and protects your account.
  • Diverse payment options cater to various player preferences.

Practical details for online casinos in South Africa

Today’s online casinos deliver a rich lineup of features tailored for South African players. For instance, many platforms offer a welcome bonus that can go as high as R5,000, plus additional perks like free bets and free spins. Additionally, the variety of games is impressive, with over 1,000 premium slots available for players to enjoy. This diverse selection caters to different tastes, ensuring everyone finds something appealing.

Besides extensive gameplay options, casinos focus on fast withdrawal processes. Players appreciate the ability to access their winnings swiftly, which enhances trust and satisfaction. Coupled with secure ZAR payment methods, managing your funds becomes both convenient and safe, allowing you to focus on the gaming experience itself.

  • Welcoming bonuses enhance initial deposits, boosting your starting balance.
  • Over 1,000 games provide a wide range of options for all players.
  • Fast withdrawals ensure quick access to your winnings.

With these practical aspects in mind, you can better appreciate the clear advantages of engaging with online casinos in South Africa.

Key benefits of online casinos

Choosing to play at an online casino comes with numerous benefits that significantly enrich your gaming experience. Among these perks are the accessibility of games and the convenience of play from home. Players can log in at any time, enjoying their favorite games without the need for travel.

  • Accessibility: Play anytime, anywhere, as long as you have an internet connection.
  • Game Variety: A vast selection of games caters to diverse preferences and interests.
  • Promotions: Regular bonuses and promotions keep the gaming experience exciting and rewarding.
  • 24/7 Availability: Many casinos operate around the clock, ensuring you can play whenever you want.

These advantages make online gaming an increasingly popular choice for players across South Africa, fostering a thriving gaming community.

Trust and security

Trust and security are paramount in the online gaming industry. Leading online casinos are licensed and regulated, ensuring they adhere to strict standards that protect players. Reputable casinos utilize advanced encryption technology to safeguard sensitive information and financial transactions, providing players with peace of mind.

Moreover, responsible gaming measures are in place to ensure a safe environment. This includes options for self-exclusion and limits on deposits and playtime, allowing players to manage their gaming responsibly. Choosing a casino that prioritizes security can greatly enhance your overall gaming experience.

  • Licensed and regulated casinos ensure compliance with industry standards.
  • Encryption technology protects personal and financial data.
  • Responsible gaming tools help players maintain control over their play.

Why choose an online casino in South Africa

Selecting an online casino in South Africa comes with distinct advantages tailored to local players. With competitive bonuses, an extensive game catalog, and user-friendly payment options, you can find a platform that aligns with your gaming needs. Additionally, the focus on customer support ensures that you have assistance whenever required, enhancing your overall experience.

In summary, the vibrant online casino scene in South Africa in 2026 offers players an array of choices. By focusing on trustworthy operators with beneficial features, you can enjoy a thrilling and rewarding gaming journey. So get started today, and experience the excitement and potential rewards that come with playing online!

Beste casinospillene du ikke må gå glipp av

Beste casinospillene du ikke må gå glipp av

Populære spilleautomater

Spilleautomater har alltid vært en av de mest populære casinospillene, både på nett og i fysiske kasinoer. Med spennende temaer, fantastiske grafikker og varierte funksjoner, tiltrekker de spillere i alle aldre. Det er mulig å finne beste casino bonus for norske spillere blant de nyeste tilbudene. Noen av de mest kjente spilleautomatene inkluderer klassikere som “Starburst” og “Gonzo’s Quest”, som tilbyr både underholdning og muligheter for store gevinster.

I tillegg til de tradisjonelle spilleautomatene, har utviklere skapt innovative videoautomater med unike funksjoner som bonusspill og gratisspinn. Disse spillene gir en spennende opplevelse som er vanskelig å motstå, og kan virkelig løfte spillopplevelsen til nye høyder.

Kortspill med dyktighet

Kortspill som blackjack og poker krever mer enn bare flaks; de krever strategi og ferdigheter. Blackjack er et av de mest spilte kortspillene på nettet, hvor målet er å slå dealeren ved å få en hånd som er nærmere 21 enn dealerens hånd, uten å gå over. Mange spillere setter pris på strategien som går inn i dette spillet, og det finnes mange varianter som holder spillopplevelsen fersk og spennende. For de som liker å utfordre seg selv, er ansvarsfull spilling viktig, slik at man kan nyte spillet.

Poker, på den annen side, er et spill som kombinerer ferdigheter og psykologi. Det finnes mange varianter av poker, fra Texas Hold’em til Omaha, og hver variant har sine egne regler og strategier. Spillere kan delta i både cash games og turneringer, noe som gir mange muligheter for både nybegynnere og erfarne spillere.

Live kasinoopplevelser

Live kasinoer har revolusjonert måten vi spiller på nettet. Ved å bruke avansert teknologi kan spillere nå delta i ekte casinospill fra komforten av sitt eget hjem. Spill som live roulette, live blackjack og live baccarat gir en autentisk opplevelse med ekte dealere som interagerer med spillerne i sanntid.

Denne typen spilling har blitt svært populær, da den kombinerer bekvemmeligheten av online spilling med den sosiale interaksjonen man finner i fysiske kasinoer. Det gir også muligheten for spillere å kommunisere med dealeren og andre spillere, noe som forbedrer opplevelsen ytterligere.

Variasjoner av bordspill

Bordspill har alltid vært en favoritt blant casinospillere. I tillegg til klassikere som roulette og craps, finnes det mange spennende variasjoner som gir nye perspektiver på tradisjonelle spill. For eksempel, fransk roulette gir unike innsatser og regler som kan være mer fordelaktige for spilleren.

Dessuten, det finnes mange innovasjoner innen bordspill som kombinerer forskjellige elementer fra flere spill. Disse variasjonene kan gjøre spillopplevelsen mer dynamisk og underholdende, noe som er perfekt for de som ønsker å prøve noe nytt og spennende.

Informasjon om nettstedet vårt

Vår hjemmeside er dedikert til å hjelpe spillere med å finne de beste casinoene og spillene tilgjengelig i Norge. Vi tilbyr grundige vurderinger av ulike nettcasinoer, basert på kriterier som spillutvalg, bonuser, betalingsmetoder og sikkerhet. Vårt mål er å gi deg en trygg og morsom spillopplevelse.

I tillegg gir vi nyttige tips og veiledning for ansvarlig spilling, slik at du kan nyte spillene på en trygg måte. Enten du er en nybegynner eller en erfaren spiller, er våre ressurser designet for å forbedre din casinoupplevelse. Besøk oss for å lære mer og bli en del av vår spillfellesskap!

Experience live sports betting at best online casinos.ca: The best games and promotions to



For sports enthusiasts and casino lovers alike, live sports betting offers an exhilarating way to engage with your favorite games. Online casinos have revolutionized the gambling experience by providing players with the ability to place bets in real-time while enjoying a rich assortment of games and promotions. Engaging in live sports betting not only elevates the excitement of watching sports but also presents significant opportunities for strategic gameplay, especially when considering the best sports betting sites canada that complement your casino experience. This article will unpack essential tips about live sports betting and highlight some of the best games and promotions available at trusted online casinos.

How account setup, payments, and play fit together

Setting up an account for live sports betting at online casinos is a straightforward process that paves the way for thrilling experiences. Understanding how account creation, payment methods, and game selection work together is crucial for a seamless betting experience. Players can benefit from various payment options like Interac, Visa, and cryptocurrencies, ensuring that their deposits and withdrawals are processed efficiently. Each casino may have different protocols, but the fundamental aspects remain fairly similar, designed to provide users with a user-friendly experience while ensuring security and accessibility.

Moreover, these online platforms often come equipped with intuitive interfaces that allow you to navigate easily between sports events, live dealer games, and promotional offers. This cohesiveness enhances the overall gaming experience and makes it easy for players to get into the action without unnecessary delays.

Getting started with live sports betting

The world of live sports betting can seem overwhelming initially, but it becomes manageable with a clear plan. Here are the essential steps to get started:

  1. Create an Account: Sign up on a trusted online casino platform to begin your journey. You’ll typically need to provide some personal information and choose a secure password.
  2. Verify Your Details: Most platforms will require verification of your identity and payment methods. This step is crucial for your security.
  3. Make a Deposit: Choose from a variety of payment methods like Visa or Bitcoin to fund your account. Make sure to check for any deposit bonuses that may be available.
  4. Select Your Game: Browse the sports events available for betting. Look for markets that pique your interest and offer the best odds.
  5. Start Betting: Place your bets! Take advantage of your knowledge and intuition to make informed decisions.
  • Easy account setup process to get you started quickly.
  • Variety of payment options cater to different preferences.
  • Immediate access to a wide range of sporting events for betting.

Practical details for engaging in live sports betting

When participating in live sports betting, several key details can enhance your experience. Many online sportsbooks offer live streaming options, allowing you to watch games as they unfold. This feature not only elevates the excitement but also enables you to make informed betting decisions based on real-time developments. Moreover, casinos often provide comprehensive statistics and live scores, making it easier for players to track their bets.

  • Live streaming of events for real-time engagement.
  • Access to updated statistics and scores that help inform your bets.
  • Promotional offers that refresh regularly to enhance your betting experience.

Additionally, familiarizing yourself with various betting types, such as point spreads, money lines, and over/under bets, allows you to diversify your betting strategy. By understanding these facets, you can tailor your betting approach to different sports and events, ultimately maximizing your potential payout.

Key benefits of sports betting at online casinos

Live sports betting through reputable online casinos comes with several advantages that can significantly enhance your gaming experience. Unlike traditional betting, online platforms provide players with unparalleled convenience, enabling them to place bets from the comfort of their homes or while on the go. This level of accessibility is further augmented by mobile-friendly interfaces designed for seamless betting on smartphones.

  • Convenient access to a wide variety of sports and betting markets.
  • Enhanced promotional offers that provide extra value.
  • Instant withdrawals available at select casinos, enhancing cash flow.
  • Real-time betting options that add excitement to every game.

Furthermore, many platforms now offer bonuses such as deposit matches and free spins, which serve as excellent opportunities for players to build their bankroll without risking personal funds. Engaging actively during promotions can lead to a more rewarding experience, encouraging users to explore new games and betting markets.

Trust and security in online sports betting

Trust and security are paramount when engaging in online sports betting. Leading casinos employ advanced encryption methods to protect your personal and financial information, ensuring that your transactions remain secure. Additionally, most reputable online casinos hold licenses from recognized authorities, such as Curaçao or Anjouan, which play a significant role in maintaining industry standards and enforcing fairness in gaming practices.

It is essential to choose platforms that have a solid reputation and provide transparency in their operations. Customer reviews and ratings can offer insight into the reliability and trustworthiness of the casino, giving you the assurance needed to place your bets with confidence.

Why choose online sports betting platforms

Online sports betting offers an engaging blend of excitement, strategy, and potential rewards. By maximizing the benefits of promotions, utilizing multiple payment methods, and enhancing your betting knowledge, you can significantly enhance your wagering experience. Choosing an online casino that prioritizes security, user satisfaction, and innovative features will only further your chances of enjoying a successful venture into live sports betting.

As you explore the various opportunities that online casinos offer, remember to prioritize responsible gaming practices, make informed decisions, and take full advantage of the promotions designed to enhance your playing experience. Happy betting!

New Online Casino Canada 2026: Dive into a world of thrilling games and promotions



The online casino landscape in Canada is rapidly evolving, especially as we step into 2026. With the introduction of new platforms offering an exciting array of games and generous promotions, players are spoiled for choice. This article explores the thrilling opportunities available at new online casinos, including enhanced security protocols, diverse payment methods, and the best online casino enticing welcome bonuses tailored for Canadian players.

How trust, access, and rewards connect in casino

Trust, access, and rewards are pivotal aspects of the online casino experience. A trustworthy casino offers verified licenses, ensuring that players can engage in gaming without concerns about fairness or security. With many new online casinos opening from July 2025 to March 2026, players can find ample options that emphasize these critical factors. Moreover, accessibility is enhanced through various payment methods such as Interac, Visa, and cryptocurrencies, making transactions seamless and efficient.

Additionally, rewards play a crucial role in drawing players to new online casinos. With welcome bonuses reaching up to C$5,000 plus 300 free spins, players can maximize their initial deposits while enjoying a broader game selection. This combination of trust, access, and rewards sets the stage for an exhilarating gaming experience tailored to meet the unique needs of Canadian players.

How to get started with new online casinos

Beginning your journey at a new online casino is an exciting endeavor, especially in the Canadian market of 2026. Here’s a step-by-step guide to ensure a smooth experience:

  1. Create an Account: Register by providing your details such as name, email, and address.
  2. Verify Your Details: Confirm your identity by submitting necessary documents as per the casino’s requirements.
  3. Make a Deposit: Choose your preferred payment method—options include Interac, credit cards, and cryptocurrencies.
  4. Select Your Game: Browse through the diverse game selection, including slots, table games, and live dealer options.
  5. Start Playing: Engage with your chosen game and enjoy the thrill of online gaming.
  • Seamlessly create an account for easy access
  • Quick verification ensures your security
  • Diverse payment options cater to all players

Practical details for new online casinos

The practical side of joining a new online casino includes understanding the features that enhance your gaming experience. New casinos in Canada often offer competitive welcome bonuses, which can significantly boost your starting bankroll. In 2026, players can expect bonuses that reach as high as C$5,000 along with hundreds of free spins, making it an ideal time to explore new platforms.

Furthermore, the average withdrawal time is impressively quick, with players generally receiving their funds within 20 minutes to 5 days, depending on the chosen payment method. This efficiency allows for a more enjoyable gaming experience, as players are not left waiting for extended periods. Plus, deposits can often be made in Canadian dollars, eliminating the hassle of currency conversion.

  • Up to C$5,000 in welcome bonuses
  • Fast payouts averaging 20 minutes to 5 days
  • Games available in Canadian dollars for convenience

With crucial details like these, players can make informed decisions and quickly adapt to the online casino environment.

Key benefits of exploring new online casinos

There are numerous compelling reasons to consider new online casinos in Canada. Many of these platforms are designed with user experience in mind, featuring intuitive interfaces and a plethora of gaming options. Additionally, the competitive nature of the industry drives these casinos to offer lucrative bonuses and promotions to attract players.

  • Access to a diverse game selection, including innovative titles
  • Exclusive promotions for new players, enhancing the welcome experience
  • Advanced security protocols ensuring safe gaming
  • Responsive customer support for prompt assistance

These benefits not only enhance the gaming experience but also provide players with peace of mind, knowing they are in a secure environment filled with exciting opportunities.

Trust and security in online casinos

Trust and security are paramount in choosing a reputable online casino. Most new casinos are licensed and regulated, ensuring they adhere to strict guidelines that protect players. This includes using advanced encryption technologies to safeguard personal and financial information, thereby fostering a safe gaming atmosphere.

Moreover, many online casinos are committed to responsible gaming practices, providing tools and resources for players to manage their gambling effectively. This commitment to security and player welfare ensures that players can enjoy their gaming experience without unnecessary worry.

  • Highly regulated environments for player protection
  • Encryption technologies safeguarding personal data
  • Responsible gaming resources available for support

Why choose new online casinos in Canada

Choosing new online casinos in Canada offers players an exciting and rewarding gaming experience. With a focus on innovative games, generous bonuses, and robust security measures, these platforms are tailored to meet the needs of modern players. As the online casino landscape continues to evolve, players can find a myriad of options that cater specifically to their preferences, ensuring that every gaming session is thrilling and enjoyable.

Embrace the adventure and excitement that new online casinos bring in 2026. With trustworthy platforms, fast payouts, and enticing promotions, players are invited to dive into a world of entertainment and rewards.